package com.liuqianfei.markme.rules.block;

import com.liuqianfei.markme.common.Utils;
import com.liuqianfei.markme.core.Token;
import com.liuqianfei.markme.rules.AbstractState;
import com.liuqianfei.markme.rules.IAction;
import com.liuqianfei.util.RegExpUtil;
import com.liuqianfei.util.StringUtil;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * GFM table, non-standard
 * @author liuqianfei
 */
public class TableAction implements IAction
{
    @Override
    public boolean parse(AbstractState state, boolean silent, int startLine, int endLine)
    {
        char ch;
        String lineText;
        String[] columns;
        int pos;
        int i;
        int nextLine;
        int columnCount;
        Token token;
        List<String> aligns;
        String t;
        int[] tableLines;
        int[] tbodyLines;

        // should have at least three lines
        if (startLine + 2 > endLine) { return false; }

        nextLine = startLine + 1;

        if (state.getsCount(nextLine) < state.getBlkIndent()) { return false; }

        // first character of the second line should be '|', '-', ':',
        // and no other characters are allowed but spaces;
        // basically, this is the equivalent of /^[-:|][-:|\s]*$/ regexp

        pos = state.getbMarks(nextLine) + state.gettShift(nextLine);
        if (pos >= state.geteMarks(nextLine)) { return false; }

        ch = state.getSrc().charAt(pos++);
        if (ch != 0x7C/* | */ && ch != 0x2D/* - */ && ch != 0x3A/* : */) { return false; }

        while (pos < state.geteMarks(nextLine))
        {
            ch = state.getSrc().charAt(pos);

            if (ch != 0x7C/* | */ && ch != 0x2D/* - */ && ch != 0x3A/* : */ && !Utils.isSpace(ch)) { return false; }

            pos++;
        }

        lineText = getLine(state, startLine + 1);

        columns = lineText.split("\\|");
        aligns = new ArrayList<>();
        for (i = 0; i < columns.length; i++)
        {
            t = columns[i].trim();
            if (StringUtil.isEmpty(t))
            {
                // allow empty columns before and after table, but not in between columns;
                // e.g. allow ` |---| `, disallow ` ---||--- `
                if (i == 0 || i == columns.length - 1)
                    continue;
                else
                    return false;
            }

            if (!RegExpUtil.match("^:?-+:?$", t, false)) { return false; }
            if (t.charAt(t.length() - 1) == 0x3A/* : */)
            {
                aligns.add(t.charAt(0) == 0x3A/* : */ ? "center" : "right");
            }
            else if (t.charAt(0) == 0x3A/* : */)
            {
                aligns.add("left");
            }
            else
            {
                aligns.add("");
            }
        }

        lineText = getLine(state, startLine).trim();
        if (lineText.indexOf('|') == -1) { return false; }
        columns = escapedSplit(lineText.replaceAll("^\\||\\|$", ""));

        // header row will define an amount of columns in the entire table,
        // and align row shouldn't be smaller than that (the rest of the rows can)
        columnCount = columns.length;
        if (columnCount > aligns.size()) { return false; }

        if (silent) { return true; }

        token     = state.push("table_open", "table", 1);
        token.map = tableLines = new int[]{startLine, 0};

        token     = state.push("thead_open", "thead", 1);
        token.map = new int[]{startLine, startLine + 1};

        token     = state.push("tr_open", "tr", 1);
        token.map = new int[]{startLine, startLine + 1};

        for (i = 0; i < columns.length; i++)
        {
            token          = state.push("th_open", "th", 1);
            token.map      = new int[]{startLine, startLine + 1};
            if (StringUtil.isNotEmpty(aligns.get(i)))
            {
                Map<String, String> attrs = new HashMap<>();
                attrs.put("style", "text-align:" + aligns.get(i));
                token.attrs  = attrs;
            }

            token          = state.push("inline", "", 0);
            token.content  = columns[i].trim();
            token.map      = new int[]{startLine, startLine + 1};
            token.children = new ArrayList<>();

            token          = state.push("th_close", "th", -1);
        }

        token     = state.push("tr_close", "tr", -1);
        token     = state.push("thead_close", "thead", -1);

        token     = state.push("tbody_open", "tbody", 1);
        token.map = tbodyLines = new int[]{startLine + 2, 0};

        for (nextLine = startLine + 2; nextLine < endLine; nextLine++) {
            if (state.getsCount(nextLine) < state.getBlkIndent()) { break; }

            lineText = getLine(state, nextLine);
            if (lineText.indexOf('|') == -1) { break; }

            // keep spaces at beginning of line to indicate an empty first cell, but
            // strip trailing whitespace
            columns = escapedSplit(lineText.replaceAll("^\\||\\|\\s*$", ""));

            token = state.push("tr_open", "tr", 1);
            for (i = 0; i < columnCount; i++)
            {
                token          = state.push("td_open", "td", 1);
                if (StringUtil.isNotEmpty(aligns.get(i)))
                {
                    Map<String, String> attrs = new HashMap<>();
                    attrs.put("style", "text-align:" + aligns.get(i));
                    token.attrs  = attrs;
                }

                token          = state.push("inline", "", 0);
                token.content  = StringUtil.isNotEmpty(columns[i]) ? columns[i].trim() : "";
                token.children = new ArrayList<>();

                token          = state.push("td_close", "td", -1);
            }
            token = state.push("tr_close", "tr", -1);
        }
        token = state.push("tbody_close", "tbody", -1);
        token = state.push("table_close", "table", -1);

        tableLines[1] = tbodyLines[1] = nextLine;
        state.setLine(nextLine);
        return true;
    }

    private String getLine(AbstractState state, int line)
    {
        int pos = state.getbMarks(line) + state.getBlkIndent(), max = state.geteMarks(line);

        return StringUtil.substr(state.getSrc(), pos, max - pos);
    }

    private String[] escapedSplit(String str)
    {
        List<String> result = new ArrayList<>();
        int pos = 0, max = str.length(), escapes = 0, lastPos = 0, lastBackTick = 0;
        char ch;
        boolean backTicked = false;

        while (pos < max)
        {
            ch  = str.charAt(pos);

            if (ch == 0x60/* ` */)
            {
                if (backTicked)
                {
                    // make \` close code sequence, but not open it;
                    // the reason is: `\` is correct code block
                    backTicked = false;
                    lastBackTick = pos;
                }
                else if (escapes % 2 == 0)
                {
                    backTicked = true;
                    lastBackTick = pos;
                }
            }
            else if (ch == 0x7c/* | */ && (escapes % 2 == 0) && !backTicked)
            {
                result.add(str.substring(lastPos, pos));
                lastPos = pos + 1;
            }

            if (ch == 0x5c/* \ */)
                escapes++;
            else
                escapes = 0;

            pos++;

            // If there was an un-closed backtick, go back to just after
            // the last backtick, but as if it was a normal character
            if (pos == max && backTicked)
            {
                backTicked = false;
                pos = lastBackTick + 1;
            }
        }

        result.add(str.substring(lastPos));

        return result.toArray(new String[result.size()]);
    }
}
